/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */package com.nth.android.image;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;

import com.nth.android.Util;

/**
 * This helper class download images from the Internet and binds those with the
 * provided ImageView.
 * 
 * <p>
 * It requires the INTERNET permission, which should be added to your
 * application's manifest file.
 * </p>
 * 
 * A local cache of downloaded images is maintained internally to improve
 * performance.
 * 
 * @author Tomislav Novoselec
 * @version 1.0.0
 * @since 1.0.0
 */
public class ImageDownloader {
	private static final String LOG_TAG = "ImageDownloader";

	private static final int HARD_CACHE_CAPACITY = 20;
	private static final int DELAY_BEFORE_PURGE = 30 * 1000; // in milliseconds

	// private Context context;
	String externalStorage;

	private static ImageDownloader INSTANCE;

	private ImageDownloader() {

	}

	// private BitmapDrawable placeholder;

	public static ImageDownloader getInstance() {
		if (INSTANCE == null) {
			INSTANCE = new ImageDownloader();
			// INSTANCE.setContext(context);
		}
		return INSTANCE;
	}

	// Hard cache, with a fixed maximum capacity and a life duration
	private final HashMap<String, Bitmap> sHardBitmapCache = new LinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY / 2, 0.75f, true) {
		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;

		@Override
		protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) {
			if (size() > HARD_CACHE_CAPACITY) {
				// Entries push-out of hard reference cache are transferred to
				// soft reference cache
				sSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue()));
				return true;
			}
			return false;
		}
	};

	// Soft cache for bitmap kicked out of hard cache
	final static ConcurrentHashMap<String, SoftReference<Bitmap>> sSoftBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>(
			HARD_CACHE_CAPACITY / 2);

	private final Handler purgeHandler = new Handler();

	private final Runnable purger = new Runnable() {
		@Override
		public void run() {
			clearCache();
		}
	};

	/**
	 * Download the specified image from the Internet and binds it to the
	 * provided ImageView. The binding is immediate if the image is found in the
	 * cache and will be done asynchronously otherwise. A null bitmap will be
	 * associated to the ImageView if an error occurs.
	 * 
	 * @param url
	 *            The URL of the image to download.
	 * @param imageView
	 *            The ImageView to bind the downloaded image to.
	 */
	public void download(String url, ImageView imageView) {
		download(url, imageView, null, false);
	}

	public void download(String url, ImageView imageView, boolean doFileCache) {
		download(url, imageView, null, doFileCache);
	}

	/**
	 * Same as {@link #download(String, ImageView)}, with the possibility to
	 * provide an additional cookie that will be used when the image will be
	 * retrieved.
	 * 
	 * @param url
	 *            The URL of the image to download.
	 * @param imageView
	 *            The ImageView to bind the downloaded image to.
	 * @param cookie
	 *            A cookie String that will be used by the http connection.
	 */
	public void download(String url, ImageView imageView, String cookie, boolean doFileCache) {
		resetPurgeTimer();
		Bitmap bitmap = getBitmapFromCache(url, imageView);

		if (bitmap == null) {
			forceDownload(url, imageView, cookie, doFileCache);
		} else {
			cancelPotentialDownload(url, imageView);
			if (imageView != null) {
				imageView.setImageBitmap(bitmap);
				Animation anim = AnimationUtils.loadAnimation(imageView.getContext(), android.R.anim.fade_in);
				imageView.startAnimation(anim);
			}
		}
	}

	/*
	 * Same as download but the image is always downloaded and the cache is not
	 * used. Kept private at the moment as its interest is not clear. private
	 * void forceDownload(String url, ImageView view) { forceDownload(url, view,
	 * null); }
	 */

	/**
	 * Same as download but the image is always downloaded and the cache is not
	 * used. Kept private at the moment as its interest is not clear.
	 */
	private void forceDownload(String url, ImageView imageView, String cookie, boolean doFileCache) {
		// State sanity: url is guaranteed to never be null in
		// DownloadedDrawable and cache keys.
		if (url == null) {
			imageView.setImageDrawable(null);
			return;
		}

		if (cancelPotentialDownload(url, imageView)) {
			// BitmapDownloaderTask task = new BitmapDownloaderTask(imageView,
			// doFileCache);
			// DownloadedDrawable downloadedDrawable = new
			// DownloadedDrawable(task);
			// if (imageView != null) {
			// imageView.setImageDrawable(downloadedDrawable);
			//
			// Animation anim =
			// AnimationUtils.loadAnimation(imageView.getContext(),
			// android.R.anim.fade_in);
			// imageView.startAnimation(anim);
			// }
			// task.execute(url, cookie);

			BitmapDownloaderTask task = new BitmapDownloaderTask(imageView, doFileCache);
			// DownloadedDrawable downloadedDrawable = new
			// DownloadedDrawable(task);
			DownloadProgressDrawable downloadedDrawable = new DownloadProgressDrawable(task);
			downloadedDrawable.setIntrinsicWidth(50);
			downloadedDrawable.setIntrinsicHeight(50);
			downloadedDrawable.getFillPaint().setColor(Color.GRAY);
			imageView.setImageDrawable(downloadedDrawable);
			task.execute(url, cookie);
		}
	}

	/**
	 * Clears the image cache used internally to improve performance. Note that
	 * for memory efficiency reasons, the cache will automatically be cleared
	 * after a certain inactivity delay.
	 */
	public void clearCache() {
		sHardBitmapCache.clear();
		sSoftBitmapCache.clear();
	}

	private void resetPurgeTimer() {
		purgeHandler.removeCallbacks(purger);
		purgeHandler.postDelayed(purger, DELAY_BEFORE_PURGE);
	}

	/**
	 * Returns true if the current download has been canceled or if there was no
	 * download in progress on this image view. Returns false if the download in
	 * progress deals with the same url. The download is not stopped in that
	 * case.
	 */
	private static boolean cancelPotentialDownload(String url, ImageView imageView) {
		BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);

		if (bitmapDownloaderTask != null) {
			String bitmapUrl = bitmapDownloaderTask.url;
			if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) {
				bitmapDownloaderTask.cancel(true);
			} else {
				imageView.setScaleType(ScaleType.CENTER);
				return false;
			}
		}
		return true;
	}

	/**
	 * @param imageView
	 *            Any imageView
	 * @return Retrieve the currently active download task (if any) associated
	 *         with this imageView. null if there is no such task.
	 */
	static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) {
		if (imageView != null) {
			Drawable drawable = imageView.getDrawable();
			if (drawable instanceof DownloadedDrawable) {
				DownloadedDrawable downloadedDrawable = (DownloadedDrawable) drawable;
				return downloadedDrawable.getBitmapDownloaderTask();
			}
			if (drawable instanceof DownloadProgressDrawable) {
				DownloadProgressDrawable downloadedDrawable = (DownloadProgressDrawable) drawable;
				return downloadedDrawable.getBitmapDownloaderTask();
			}
		}
		return null;
	}

	/**
	 * @param url
	 *            The URL of the image that will be retrieved from the cache.
	 * @return The cached bitmap or null if it was not found.
	 */
	private Bitmap getBitmapFromCache(String url, ImageView image) {

		// First try the hard reference cache
		synchronized (sHardBitmapCache) {
			final Bitmap bitmap = sHardBitmapCache.get(url);
			if (bitmap != null) {
				// Bitmap found in hard cache
				// Move element to first position, so that it is removed last
				sHardBitmapCache.remove(url);
				sHardBitmapCache.put(url, bitmap);
				return bitmap;
			}
		}

		// Then try the soft reference cache
		SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(url);
		if (bitmapReference != null) {
			final Bitmap bitmap = bitmapReference.get();
			if (bitmap != null) {
				// Bitmap found in soft cache
				return bitmap;
			}
			// Soft reference has been Garbage Collected
			sSoftBitmapCache.remove(url);
		}

		// if (image != null && image.getContext() != null) {
		// final Bitmap bitmap = Util.getBitmapFromFilesDir(image.getContext(),
		// url);
		// if (bitmap != null) {
		// return bitmap;
		// }
		// }

		if (image != null && !TextUtils.isEmpty(externalStorage)) {
			Bitmap bitmap = null;
			try {
				bitmap = Util.getBitmapFromExternalStorage(externalStorage, url);
			} catch (GeneralSecurityException e) {
				Log.e("GSE", e.getMessage());
				return null;
			}
			if (bitmap != null) {
				return bitmap;
			}
		}
		return null;
	}

	/**
	 * The actual AsyncTask that will asynchronously download the image.
	 */
	class BitmapDownloaderTask extends AsyncTask<String, Long, Bitmap> {
		private static final int IO_BUFFER_SIZE = 4 * 1024;
		String url;
		private final WeakReference<ImageView> imageViewReference;
		private boolean doFileCache;
		@SuppressWarnings("unused")
		private ImageView imageView;
		private ScaleType scaleType;

		public BitmapDownloaderTask(ImageView imageView, boolean doFileCache) {
			imageViewReference = new WeakReference<ImageView>(imageView);
			this.doFileCache = doFileCache;
			this.imageView = imageView;
			this.scaleType = imageView.getScaleType();
		//	Log.i("Scale type before:",""+scaleType);
			imageView.setScaleType(ScaleType.CENTER);
		}

		/**
		 * Actual download method.
		 */

		@SuppressWarnings({ "null", "boxing" })
		@Override
		protected Bitmap doInBackground(String... params) {
			final DefaultHttpClient client = new DefaultHttpClient();
			url = params[0];
			final HttpGet getRequest;
			try {
				getRequest = new HttpGet(url);
			} catch (Exception e) {
				Log.e("ERROR url", e.getMessage());
				return null;
			}
			String cookie = params[1];
			if (cookie != null && getRequest != null) {
				getRequest.setHeader("cookie", cookie);
			}

			try {
				HttpResponse response = client.execute(getRequest);
				final int statusCode = response.getStatusLine().getStatusCode();
				if (statusCode != HttpStatus.SC_OK) {
					Log.w("ImageDownloader", "Error " + statusCode + " while retrieving bitmap from " + url);
					return null;
				}

				final HttpEntity entity = response.getEntity();
				if (entity != null) {
					InputStream inputStream = null;
					OutputStream outputStream = null;
					try {
						inputStream = entity.getContent();
						final ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
						outputStream = new BufferedOutputStream(dataStream, IO_BUFFER_SIZE);

						byte[] b = new byte[IO_BUFFER_SIZE];
						int read = 0;
						long bytesRead = 0;
						long bytesTotal = entity.getContentLength();
						while ((read = inputStream.read(b)) != -1) {
							outputStream.write(b, 0, read);
							bytesRead += read;
							publishProgress(bytesRead, bytesTotal);
						}

						outputStream.flush();

						Bitmap bitmap = null;
						try {
							final byte[] data = dataStream.toByteArray();
							bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
						} catch (OutOfMemoryError e) {
							Log.e("ImageDownloader out of memory error raised!", e.getMessage());
						}
						final Bitmap bitmapToSave = bitmap;
						if (doFileCache && bitmapToSave != null && !TextUtils.isEmpty(externalStorage)) {
							Thread t = new Thread(new Runnable() {

								@Override
								public void run() {
									FileOutputStream out = null;
									try {
										// out = new
										// FileOutputStream(imageView.getContext().getFilesDir()
										// + File.separator +
										// Util.hexEncode(url.getBytes()));
										out = new FileOutputStream(externalStorage + File.separator + Util.hexEncode(url.getBytes()));
										bitmapToSave.compress(Bitmap.CompressFormat.PNG, 90, out);
										out.flush();
									} catch (Exception e) {
										// DO NIX
									} finally {
										if (out != null) {
											try {
												out.close();
											} catch (IOException e) {
												// DO NIX

											}
										}
									}
								}
							});
							t.start();

						}

						return bitmap;

					} finally {
						if (inputStream != null) {
							inputStream.close();
						}
						if (outputStream != null) {
							outputStream.close();
						}
						entity.consumeContent();
					}
				}
			} catch (IOException e) {
				getRequest.abort();
				Log.w(LOG_TAG, "I/O error while retrieving bitmap from " + url, e);
			} catch (IllegalStateException e) {
				getRequest.abort();
				Log.w(LOG_TAG, "Incorrect URL: " + url);
			} catch (Exception e) {
				getRequest.abort();
				Log.w(LOG_TAG, "Error while retrieving bitmap from " + url, e);
			} finally {
				if (client != null) {
					// client.;
				}
			}
			return null;
		}

		@SuppressWarnings("boxing")
		@Override
		protected void onProgressUpdate(Long... values) {
			super.onProgressUpdate(values);
			if (imageViewReference != null && values.length >= 2) {
				final ImageView imageView = imageViewReference.get();
				final Drawable drawable = imageView.getDrawable();
				final long current = values[0];
				final long total = values[1];
				if (drawable instanceof ProgressDrawable && total > 0) {
					ProgressDrawable progressDrawable = (ProgressDrawable) drawable;
					progressDrawable.setBytesCurrent(current);
					progressDrawable.setBytesTotal(total);
				}
			}
		}

		/**
		 * Once the image is downloaded, associates it to the imageView
		 */
		@SuppressWarnings("hiding")
		@Override
		protected void onPostExecute(Bitmap bitmap) {
			if (isCancelled()) {
				bitmap = null;
			}

			// Add bitmap to cache
			if (bitmap != null) {
				synchronized (sHardBitmapCache) {
					sHardBitmapCache.put(url, bitmap);
				}
			}

			if (imageViewReference != null) {
				ImageView imageView = imageViewReference.get();
				BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
				// Change bitmap only if this process is still associated with
				// it
				if (this == bitmapDownloaderTask) {
					imageView.setScaleType(scaleType);
					imageView.setImageBitmap(bitmap);
					//Log.i("Scale type after: ","" + scaleType);
					Animation anim = AnimationUtils.loadAnimation(imageView.getContext(), android.R.anim.fade_in);
					imageView.startAnimation(anim);
				}
			}
		}

		public void copy(InputStream in, OutputStream out) throws IOException {
			byte[] b = new byte[IO_BUFFER_SIZE];
			int read;
			while ((read = in.read(b)) != -1) {
				out.write(b, 0, read);
			}
		}
	}

	/**
	 * A fake Drawable that will be attached to the imageView while the download
	 * is in progress.
	 * 
	 * <p>
	 * Contains a reference to the actual download task, so that a download task
	 * can be stopped if a new binding is required, and makes sure that only the
	 * last started download process can bind its result, independently of the
	 * download finish order.
	 * </p>
	 */
	//
	static class DownloadedDrawable extends ColorDrawable {
		private final WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference;

		public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
			super(Color.TRANSPARENT);
			bitmapDownloaderTaskReference = new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
		}

		public BitmapDownloaderTask getBitmapDownloaderTask() {
			return bitmapDownloaderTaskReference.get();
		}
	}

	public String getExternalStorage() {
		return externalStorage;
	}

	public void setExternalStorage(String externalStorage) {
		this.externalStorage = externalStorage;
	}

	static class DownloadProgressDrawable extends ProgressDrawable {

		private final WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference;

		public DownloadProgressDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
			super();
			bitmapDownloaderTaskReference = new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
		}

		public DownloadProgressDrawable(long current, long total, BitmapDownloaderTask bitmapDownloaderTask) {
			super(current, total);
			bitmapDownloaderTaskReference = new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
		}

		public BitmapDownloaderTask getBitmapDownloaderTask() {
			return bitmapDownloaderTaskReference.get();
		}

	}

	// static class DownloadedDrawable extends BitmapDrawable {
	// private final WeakReference<BitmapDownloaderTask>
	// bitmapDownloaderTaskReference;
	//
	// public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask,
	// Context context, Bitmap placeholder) {
	// super(context.getResources(), placeholder);
	// bitmapDownloaderTaskReference = new
	// WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
	// }
	//
	// public BitmapDownloaderTask getBitmapDownloaderTask() {
	// return bitmapDownloaderTaskReference.get();
	// }
	// }

	// public void setPlaceHolder(BitmapDrawable placeholder){
	// this.placeholder = placeholder;
	// }

}
